#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

#include "cfg.h"
#include "check.h"
#include "hashmap.h"
#include "ijs_backend.h"
#include "json.h"
#include "list_printers.h"
#include "oauth2.h"
#include "output_print_job.h"
#include "poll_printer_reg.h"
#include "printer.h"
#include "register_printer.h"
#include "stringprintf.h"
#include "xmpp.h"


static const char ppd[] = /***/
  "*PPD-Adobe: \"4.3\"\r\n"
  "*% =================================\r\n"
  "*% Basic Device Capabilities\r\n"
  "*% =================================\r\n"
  "*FormatVersion: \"4.3\"\r\n"
  "*Manufacturer: \"Generic\"\r\n"
  "*Product: \"(Generic Printer)\"\r\n"
  "*ModelName: \"Generic Printer\"\r\n"
  "*ShortNickName: \"Generic Printer\"\r\n"
  "*NickName: \"Generic Printer\"\r\n"
  "*LanguageLevel: \"2\"\r\n"
  "*ColorDevice: True\r\n"
  "*DefaultColorSpace: CMYK\r\n"
  "*TTRasterizer: Type42\r\n"
  "*FileSystem: False\r\n"
  "*Throughput: \"10\"\r\n"
  "*OpenUI *Resolution/Printer Resolution: PickOne\r\n"
  "*DefaultResolution: 600x1200dpi\r\n"
  "*OrderDependency: 5 DocumentSetup  *Resolution\r\n"
  "*Resolution 600x600dpi/600 dpi: \"\r\n"
  " <</HWResolution [600 600] /PreRenderingEnhance false>> setpagedevice\"\r\n"
  "*End\r\n"
  "*Resolution 600x1200dpi/FastRes 1200: \"\r\n"
  " <</HWResolution [600 600] /PreRenderingEnhance true>> setpagedevice\"\r\n"
  "*End\r\n"
  "*?Resolution: \"\r\n"
  "  save\r\n"
  "    currentpagedevice /HWResolution get\r\n"
  "    0 get\r\n"
  "    ( ) cvs print\r\n"
  "    (dpi)\r\n"
  "    = flush\r\n"
  "  restore\r\n"
  "\"\r\n"
  "*End\r\n"
  "*CloseUI: *Resolution";

static char *getValue(const struct JSON *json, const char *key) {
  check(json->type == J_OBJECT);
  struct JSON *value = (struct JSON *)getFromHashMap(&json->obj, key);
  if (value) {
    check(value->type == J_STRING);
    char *ret;
    check(ret = strdup(value->str));
    return ret;
  }
  return NULL;
}

static int setValues(struct Cfg *cfg, struct JSON **config,
                     struct JSON **printers, struct Printer *printer, ...) {
  *config = lockCfgAndGetJSON(cfg);
  check((*config)->type == J_OBJECT);
  *printers = (struct JSON *)getFromHashMap(&(*config)->obj, "printers");
  if (*printers) {
    check((*printers)->type == J_ARRAY);
  }
  if (printer) {
    if (!*printers || (*printers)->type != J_ARRAY) {
      return 0;
    }

    // The configuration file might have changed on disk, since the
    // previous time that we read it. In order to update an entry, we
    // scan through the list of printers and find the matching one.
    // If we can't find a matching printer, we assume that it has since
    // been deleted by the user, and we drop it.
    struct JSON *prt = NULL;
    for (int i = 0; i < (*printers)->array_size; ++i) {
      prt = (*printers)->array + i;
      if (prt->type == J_OBJECT) {
        if (printer->printerId) {
          // If the printer had previously been registered with the cloud, then
          // it is uniquely identified by its "id".
          const struct JSON *id = (const struct JSON *)
            getFromHashMap(&prt->obj, "id");
          if (id && id->type == J_STRING &&
              !strcmp(printer->printerId, id->str)) {
            goto found;
          }
        } else {
          // If the printer doesn't have an "id" yet, then it has recently
          // been manually added to the configuration file. It is correctly
          // identified by its parameters.
          if (getFromHashMap(&prt->obj, "id")) {
            goto mismatch;
          }
          const struct {
            const char *key, *value;
          } cmp[] = { { "name",         printer->printerName },
                      { "desc",         printer->printerDesc },
                      { "fromEmail",    printer->fromEmail },
                      { "toEmail",      printer->toEmail },
                      { "ijsDriver",    printer->ijsDriver },
                      { "printerPort",  printer->printerPort },
                      { "printerModel", printer->printerModel },
          };
          for (int j = 0; j < sizeof(cmp)/sizeof(*cmp); ++j) {
            const struct JSON *json = (const struct JSON *)
              (const struct JSON *)getFromHashMap(&prt->obj, cmp[j].key);
            if ((!cmp[j].value && json) ||
                ( cmp[j].value && (!json || json->type != J_STRING ||
                                   strcmp(cmp[j].value, json->str)))) {
              goto mismatch;
            }
          }
          goto found;
        }
      }
    mismatch:;
    }
    // If we get here, we haven't been able to find a matching printer in the
    // configuration file.
    return 0;

    // If we get here, "prt" is points to the matching printer in the
    // configuration file.
  found:;
    va_list ap;
    va_start(ap, printer);
    for (;;) {
      const char *key = va_arg(ap, const char *);
      if (!key) {
        break;
      }
      check(key = strdup(key));
      const char *value = va_arg(ap, const char *);
      struct JSON *v = NULL;
      if (value) {
        v = newJSON("\"\"");
        check(v->str = realloc(v->str, strlen(value) + 1));
        strcpy(v->str, value);
      } else {
        v = newJSON("null");
      }
      addToHashMap(&prt->obj, key, (const char *)v);
    }
    return 1;
  } else {
    return 0;
  }
}

static void removePrinter(struct Cfg *cfg, struct JSON **config,
                          struct JSON **printers, const char *id) {
  int edited = setValues(cfg, config, printers, NULL);
  if (*printers && (*printers)->type == J_ARRAY) {
    for (int i = 0; i < (*printers)->array_size; ++i) {
      struct JSON *prt = (*printers)->array + i;
      if (prt->type == J_OBJECT) {
        const struct JSON *printerId = (const struct JSON *)
          getFromHashMap(&prt->obj, "id");
        if (printerId && printerId->type == J_STRING &&
            !strcmp(id, printerId->str)) {
          destroyJSON(prt);
          memmove(prt, prt + 1,
                  ((*printers)->array_size - i - 1)*sizeof(*prt));
          --(*printers)->array_size;
          --i;
          edited = 1;
        }
      }
    }
  }
  if (edited) {
    commitCfg(cfg);
  } else {
    unlockCfg(cfg);
  }
}

static void initPrinter(struct Printer *printer, const char *cloudPrintURL,
                        const struct JSON *conf) {
  memset(printer, 0, sizeof(struct Printer));
  check(printer->cloudPrintURL= strdup(cloudPrintURL));
  check(printer->printerName  = getValue(conf, "name"));
  check(printer->printerDesc  = getValue(conf, "desc"));
        printer->printerId    = getValue(conf, "id");
        printer->jid          = getValue(conf, "jid");
        printer->refreshToken = getValue(conf, "refreshToken");
        printer->fromEmail    = getValue(conf, "fromEmail");
        printer->toEmail      = getValue(conf, "toEmail");
        printer->ijsDriver    = getValue(conf, "ijsDriver");
        printer->printerPort  = getValue(conf, "printerPort");
        printer->printerModel = getValue(conf, "printerModel");
  if (printer->toEmail) {
    check(!printer->ijsDriver);
    check(!printer->printerPort);
    check(!printer->printerModel);
  } else {
    check(!printer->fromEmail);
    check(printer->ijsDriver);
    check(printer->printerPort);
    printer->printerBackend   = newIJSPrinterBackend(printer->ijsDriver,
                                                     printer->printerPort,
                                                     printer->printerModel);
  }
}

void deleteAllPrinters(struct Printer *printer, int numPrinters) {
  for (int i = 0; i < numPrinters; ++i) {
    free((void *)printer[i].cloudPrintURL);
    free((void *)printer[i].printerName);
    free((void *)printer[i].printerDesc);
    free((void *)printer[i].fromEmail);
    free((void *)printer[i].toEmail);
    free((void *)printer[i].ijsDriver);
    free((void *)printer[i].printerPort);
    free((void *)printer[i].printerModel);
    free((void *)printer[i].printerId);
    deletePrinterBackend(printer[i].printerBackend);
    free((void *)printer[i].jid);
    free((void *)printer[i].refreshToken);
    deleteXMPP(printer[i].xmpp);
  }
  free(printer);
}

struct Printer *registerAllPrinters(struct Cfg *cfg, int *numPrinters) {
  *numPrinters             = 0;
  struct Printer *ret      = NULL;

  // Read configuration file and build data structure describing all the
  // printers that we are managing.
  struct JSON *config, *printers;
  setValues(cfg, &config, &printers, NULL);
  unlockCfg(cfg);

  // Get global configuration options
  const char *cloudPrintURL= getValue(config, "cloudprint");
  const char *oauth2URL    = getValue(config, "oauth2");
  const char *clientId     = getValue(config, "clientId");
  const char *clientSecret = getValue(config, "clientSecret");
  const char *proxyName    = getValue(config, "proxyName");
  check(cloudPrintURL && oauth2URL && clientId && clientSecret && proxyName);

  // Get information on printers
  if (printers) {
    for (int i = 0; i < printers->array_size; ++i) {
      check(ret = realloc(ret, ++*numPrinters*sizeof(struct Printer)));
      initPrinter(ret + *numPrinters - 1, cloudPrintURL, printers->array + i);
    }
  }

  // Register printers with the cloud.
  const struct JSON *list  = NULL;
  for (int i = 0; i < *numPrinters; ++i) {
    // If we have previously been registered, see if the cloud still knows
    // about this printer. If so, we can connect again. If not, then the
    // printer was deleted from the cloud and should be deleted from our
    // configuration file as well.
    if (ret[i].printerId) {
      check(ret[i].refreshToken);
      struct OAuth2 *oauth2;
      oauth2             = newOAuth2(oauth2URL, clientId, clientSecret,
                                     NULL, ret[i].refreshToken);
      if (!oauth2->accessToken) {
        // If our refresh token doesn't get recognized, there is not much
        // we can do other then re-registering the printer. As we can't
        // tell whether the printer was deleted from the cloud, or whether
        // the cloud is just temporarily malfunctioning, we decide that we
        // should not automatically add the printer back to the cloud. This
        // requires human interaction to resolve.
      stalePrinter:
        deleteOAuth2(oauth2);
        removePrinter(cfg, &config, &printers, ret[i].printerId);
        deleteAllPrinters(ret + i, 1);
        memmove(ret + i, ret + i + 1, sizeof(struct Printer) * --*numPrinters);
        --i;
        continue;
      } else {
        // Check whether the server still knows about our printer
        if (!list) {
          list = listPrinters(cloudPrintURL, oauth2, proxyName);
        }
        check(list && list->type == J_OBJECT);
        const struct JSON *cloudPrinters = (const struct JSON *)
                                      getFromHashMap(&list->obj, "printers");
        check(cloudPrinters && cloudPrinters->type == J_ARRAY);
        for (int j = 0; j < cloudPrinters->array_size; ++j) {
          const struct JSON *cloudPrinter = cloudPrinters->array + j;
          check(cloudPrinter->type == J_OBJECT);
          const struct JSON *cloudId = (const struct JSON *)
                                    getFromHashMap(&cloudPrinter->obj, "id");
          if (cloudId && cloudId->type == J_STRING &&
              !strcmp(cloudId->str, ret[i].printerId)) {
            goto printerExists;
          }
        }
        goto stalePrinter;
      printerExists:;
        ret[i].oauth2    = oauth2;
      }
    } else {
      // We found a printer in our configuration file that has never before
      // been registered with the cloud. Attempt to register it now.
      struct JSON *registration;
      check(registration = registerPrinter(cloudPrintURL, ret[i].printerName,
                                           proxyName, ret[i].printerDesc, ppd,
                                           "", "application/vnd.cups-ppd"));
    
      const struct JSON *inviteURL;
      check((inviteURL   = (const struct JSON *)
             getFromHashMap(&registration->obj, "complete_invite_url")) &&
            inviteURL->type == J_STRING);

      // The user needs to open the acceptInvitation URL and complete
      // authentication in order to register the printer with the cloud.
      /***/
      char *acceptInvitation = stringPrintf(NULL,
                                       "url=`cat <<'EOF'\n%s\nEOF\n`;"
                                       "xdg-open \"${url}\" >/dev/null 2>&1 &",
                                       inviteURL->str);
      check(!system(acceptInvitation));
      free(acceptInvitation);
      printf("Open %s to register your printer\n", inviteURL->str);
      /***/
    
      // We need to keep polling the registration URL until the user has
      // completed the registration process.
      struct JSON *pollRegistration;
      check((pollRegistration = pollPrinterRegistration(&registration->obj,
                                                        clientId)) &&
            pollRegistration->type == J_OBJECT);
      deleteJSON(registration);

      // The registration protocol requires the we display a confirmation
      // page to the user.
      const struct JSON *confirmationPageURL;
      check((confirmationPageURL = (const struct JSON *)
            getFromHashMap(&pollRegistration->obj, "confirmation_page_url")) &&
            confirmationPageURL->type == J_STRING);

      /***/
      char *confirmation_page = stringPrintf(NULL,
                                       "url=`cat <<'EOF'\n%s\nEOF\n`;"
                                       "xdg-open \"${url}\" >/dev/null 2>&1 &",
                                       confirmationPageURL->str);
      check(!system(confirmation_page));
      free(confirmation_page);
      printf("The printer is now registered, "
             "you can verify registration at %s\n",
             confirmationPageURL->str);
      /***/
    
      // Get printer information from the server's response and persist the
      // data. The next time our proxy starts up, we can reconnect to the
      // cloud with the same printer information.

      // Printer's jid for talking to XMPP server
      const struct JSON *jid;
      check((jid         = (const struct JSON *)
                         getFromHashMap(&pollRegistration->obj, "xmpp_jid")) &&
            jid->type == J_STRING);
      check(!ret[i].jid);
      check(ret[i].jid   = strdup(jid->str));

      // Printer's id for identifying it with the CloudPrint server
      const struct JSON *request;
      check((request     = (const struct JSON *)
                          getFromHashMap(&pollRegistration->obj, "request")) &&
            request->type == J_OBJECT);

      const struct JSON *params;
      check((params      = (const struct JSON *)
                                    getFromHashMap(&request->obj, "params")) &&
            params->type == J_OBJECT);

      const struct JSON *printerId;
      check((printerId   = (const struct JSON *)
                                  getFromHashMap(&params->obj, "printerid")) &&
            printerId->type == J_ARRAY && printerId->array_size == 1 &&
            printerId->array->type == J_STRING);
      check(!ret[i].printerId);

      // Printer's authorization code for getting OAuth2 tokens
      const struct JSON *authorizationCode;
      check((authorizationCode = (const struct JSON *)
             getFromHashMap(&pollRegistration->obj, "authorization_code")) &&
            authorizationCode->type == J_STRING);
      check(!ret[i].oauth2);

      // Perform OAuth2 authentication to get the initial refresh token
      struct OAuth2 *oauth2;
      check((oauth2      = newOAuth2(oauth2URL, clientId, clientSecret,
                                     authorizationCode->str, NULL)) &&
            oauth2->refreshToken);
      check(!ret[i].oauth2);
      check(!ret[i].refreshToken);
      ret[i].oauth2      = oauth2;
      check(ret[i].refreshToken = strdup(oauth2->refreshToken));

      // Persist all the new information to our configuration file
      check(setValues(cfg, &config, &printers, &ret[i],
                      "jid", ret[i].jid,
                      "id", printerId->array->str,
                      "refreshToken", ret[i].refreshToken,
                      NULL));
      check(ret[i].printerId = strdup(printerId->array->str));

      // We are done processing all the data retrieved by polling the result
      // of the registration request.
      deleteJSON(pollRegistration);

      commitCfg(cfg);
    }
    check(ret[i].oauth2->accessToken);
    check(ret[i].printerId);
    check(ret[i].jid);
    
    // Listen for notifications about new print jobs
    ret[i].xmpp          = newXMPP(ret[i].oauth2, ret[i].jid, ret[i].printerId,
                                   ret[i].printerDesc);
  }
  deleteJSON((struct JSON *)list);
  free((void *)cloudPrintURL);
  free((void *)oauth2URL);
  free((void *)clientId);
  free((void *)clientSecret);
  free((void *)proxyName);

  return ret;
}
